﻿using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Sustainalytics.ClientDW.Entities.Universes;
using Sustainalytics.DataLayer;
using Sustainalytics.Entities;
using Sustainalytics.Entities.ESGRatings;
using Sustainalytics.Entities.ScreeningTool;
using Sustainalytics.MongoDB.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Sustainalytics.ScreeningTool.BL
{
    public class ScreeningDataRepository : IScreeningDataRepository
    {
        private readonly IMongoCollection<ScreeningData> _screeningDataCollection;
        private static readonly string CUSIP_NAME = "CUSIP";
        private static readonly string ISIN_NAME = "ISIN";

        public static IScreeningDataRepository GetScreeningDataRepository(string connectionString)
        {
            return new ScreeningDataRepository(connectionString);
        }

        private ScreeningDataRepository(string connectionString)
        {
            var dbConnectionString = MongoUrl.Create(connectionString);

            if (string.IsNullOrWhiteSpace(dbConnectionString.DatabaseName))
            {
                throw new ArgumentException("No database specified in the connection string!");
            }

            var client = new MongoClient(dbConnectionString);
            var database = client.GetDatabase(dbConnectionString.DatabaseName);

            _screeningDataCollection = MongoFactory.GetCollectionFromDatabase<ScreeningData>(connectionString);
        }

        public IEnumerable<double> LoadData(IEnumerable<int> universeIds, string criteriaEntityField)
        {
            var builder = Builders<ScreeningData>.Filter;
            var filter = builder.AnyIn(x => x.UniverseIds, universeIds) & builder.Exists(criteriaEntityField);
            var projection = Builders<ScreeningData>.Projection.Exclude(x => x.Id).Include(criteriaEntityField);

            return _screeningDataCollection
                .Find(filter)
                .Project(projection)
                .ToList()
                .Select(bsonDocument =>
                {
                    var bsonValue = bsonDocument[criteriaEntityField];

                    if (criteriaEntityField.EndsWith("Rank", StringComparison.Ordinal) ||
                        criteriaEntityField.StartsWith("CG - Rank - ", StringComparison.Ordinal) ||
                        criteriaEntityField.EndsWith("Rank-Universe", StringComparison.Ordinal) ||
                        criteriaEntityField.EndsWith("Rank-Subindustry", StringComparison.Ordinal))
                        return BsonSerializer.Deserialize<PeerRanking>(bsonValue as BsonDocument).Rank;

                    if (bsonValue.IsInt32)
                        return bsonValue.AsInt32;

                    if (bsonValue.IsInt64)
                        return bsonValue.AsInt64;

                    return bsonValue.AsDouble;
                });
        }

        public IEnumerable<Guid> Find(IEnumerable<int> universeIds, Func<FilterDefinitionBuilder<ScreeningData>, FilterDefinition<ScreeningData>> predicate)
        {
            var builder = Builders<ScreeningData>.Filter;
            var filter = builder.AnyIn(x => x.UniverseIds, universeIds) & predicate.Invoke(builder);
            return _screeningDataCollection.Find(filter).Project(x => x.Id).ToList();
        }

        public List<BsonDocument> GetCompaniesForScreening(Filter filter, List<int> accountUniverseIds, List<Guid> companiesInPorfolio)
        {
            FilterDefinition<ScreeningData> query = FilterDefinition<ScreeningData>.Empty;

            var selectedUniverse = new List<int>();
            if (filter != null && filter.UniverseId != null)
                selectedUniverse.Add(filter.UniverseId.Value);

            ApplyFilterSettings(accountUniverseIds, filter, selectedUniverse, companiesInPorfolio, ref query);

            var companyData = _screeningDataCollection
                .Find(query)
                .Project<BsonDocument>(Builders<ScreeningData>.Projection
                    .Include(x=> x.Id)
                    .Include(x=> x.UniverseIds))
                 .ToList();

            return companyData;
        }

        public Dictionary<Guid, BsonDocument> GetScreeningDataDictionary(string[] fields, List<Guid> companyIds, bool hasISINClaim = false, bool hasCUSIPClaim = false)
        {
            if (!companyIds.Any())
                return new Dictionary<Guid, BsonDocument>();

            var query = Builders<ScreeningData>.Filter.In(x => x.Id, companyIds);

            var companyData = _screeningDataCollection
                .Find(query)
                .Project(GetProjectionDefinitionListOfStrings(fields))
                .ToList();

            foreach (BsonDocument doc in companyData)
            {
                if (fields.Contains(ISIN_NAME) && !hasISINClaim && doc.Contains(ISIN_NAME) &&
                    (doc[ISIN_NAME].AsString.StartsWith("US") || doc[ISIN_NAME].AsString.StartsWith("CA")))
                {
                    doc[ISIN_NAME] = "No Access";
                }
                if (fields.Contains(CUSIP_NAME) && !hasCUSIPClaim)
                {
                    doc[CUSIP_NAME] = "No Access";
                }
            }

            return companyData.ToDictionary(c => c["_id"].AsGuid, c => c);
        }

        private static string GetProjectionDefinitionListOfStrings(string[] fields)
        {
            StringBuilder projectionStringBuilder = new StringBuilder();

            projectionStringBuilder.Append("{ ");
            foreach (var field in fields)
            {
                projectionStringBuilder.Append($"\'{field}\': 1, ");
            }

            projectionStringBuilder.Append(" }");

            return projectionStringBuilder.ToString();
        }

        public List<BsonDocument> ListCriteriaResults(List<string> indicators, List<int> universeIds, Filter filter, List<Guid> companiesInPorfolio)
        {
            FilterDefinition<ScreeningData> query = FilterDefinition<ScreeningData>.Empty;

            var fields = new List<string> { "_id", "UniverseIds", nameof(ScreeningData.ResearchStates) };

            fields.AddRange(indicators);

            var universe = new List<int>();

            if (filter.UniverseId != null)
                universe.Add(filter.UniverseId.Value);

            ApplyFilterSettings(universeIds, filter, universe, companiesInPorfolio, ref query);

            return _screeningDataCollection.Find(query).Project(GetProjectionDefinitionListOfStrings(fields.Distinct().ToArray())).ToList();
        }

        public List<BsonDocument> GetCompaniesData(Filter filter, List<int> accountUniverseIds, List<Universe> accountUniverses,
            List<PortfolioDetails> portfolioDetails, List<FilterCriteria> filterCriteria, List<Guid> companiesInPorfolio, bool hasISINClaim, bool hasCUSIPClaim)
        {
            
            var criteriaType4ExtraFields = new List<string>();
            if (filterCriteria.Any(x=> x.CriteriaType == CriteriaTypeEnum.CriteriaType4))
            {
                filterCriteria.Where(x=> x.CriteriaType == CriteriaTypeEnum.CriteriaType4).ToList().ForEach(x =>
                {
                    var stringFields = x.CriteriaEntityField.Split(' ');
                    criteriaType4ExtraFields.Add($"{stringFields[0]} {stringFields[1]} Category of Involvement");
                });
            }


            List<BsonDocument> companyData = null;
            try
            {
                var criterias = filterCriteria
                    .Select(x => x.CriteriaEntityField)
                    .Concat(criteriaType4ExtraFields)
                    .Concat(new[]
                    {
                    "_id",
                    "CompanyName",
                    "CompanyNameLowerCase",
                    "CompanyNameUrlFriendly",
                    "ResearchParentId",
                    "CapitalIqId",
                    "Company type",
                    "TemplateType",

                    "UniverseIds",
                    "MainExchange",
                    "MainTicker",

                    nameof(ScreeningData.ResearchStates),
                    })
                    .Distinct()
                    .ToArray();

                var query = GetCompaniesQuery(filter, accountUniverseIds, accountUniverses, companiesInPorfolio);

                companyData = _screeningDataCollection.Find(query).Project(GetProjectionDefinitionListOfStrings(criterias)).ToList();
                foreach(BsonDocument doc in companyData)
                {
                    if(filterCriteria.Exists(x=>x.Name == ISIN_NAME) && !hasISINClaim && doc.Contains(ISIN_NAME) &&
                        (doc[ISIN_NAME].AsString.StartsWith("US") || doc[ISIN_NAME].AsString.StartsWith("CA")))
                    {
                        doc[ISIN_NAME] = "No Access";
                    }
                    if (filterCriteria.Exists(x => x.Name == CUSIP_NAME) && !hasCUSIPClaim)
                    {
                        doc[CUSIP_NAME] = "No Access";
                    }
                }

                return MergeBsonDocumentWithPortfolioData(companyData, portfolioDetails);
            }
            finally
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
            }
        }

        public List<BsonDocument> GetCompaniesData(List<PortfolioDetails> portfolioDetails, List<FilterCriteria> filterCriteria, FilterDefinition<ScreeningData> query, int skip, int take)
        {
            var criterias = filterCriteria
                .Select(x => x.CriteriaEntityField)
                .Concat(new[]
                {
                //"_id",
                "CompanyName",
                "CompanyNameLowerCase",
                "CompanyNameUrlFriendly",
                "ResearchParentId",
                "CapitalIqId",
                "Company type",
                "TemplateType",

                "UniverseIds",
                "MainExchange",
                "MainTicker",

                nameof(ScreeningData.ResearchStates),
                })
                .Distinct()
                .ToArray();

            var companyData = _screeningDataCollection.Find(Builders<ScreeningData>.Filter.Empty)
                .Skip(skip)
                .Limit(take)                
                .Project(GetProjectionDefinitionListOfStrings(criterias))
                .ToList();

            List<BsonDocument> companyDataDocuments = companyData.Select(x => x.ToBsonDocument()).ToList();

            return MergeBsonDocumentWithPortfolioData(companyDataDocuments, portfolioDetails);
        }

        public FilterDefinition<ScreeningData> GetCompaniesQuery(Filter filter, List<int> accountUniverseIds, List<Universe> accountUniverses, List<Guid> companiesInPorfolio)
        {
            FilterDefinition<ScreeningData> query = FilterDefinition<ScreeningData>.Empty;

            if (filter != null)
            {
                var selectedUniverse = new List<int>();
                if (filter.UniverseId != null)
                {
                    var singleOrDefault = accountUniverses.SingleOrDefault(p => p.Name == filter.UniverseName);

                    if (singleOrDefault != null)
                        selectedUniverse.Add(singleOrDefault.Id);
                }

                ApplyFilterSettings(accountUniverseIds, filter, selectedUniverse, companiesInPorfolio, ref query);
            }

            return query;
        }

        public List<BsonDocument> GetCompaniesDataAllAccounts(Filter filter, List<int> accountUniverseIds, List<Universe> accountUniverses,
            List<PortfolioDetails> portfolioDetails, List<FilterCriteria> filterCriteria, List<Guid> companiesInPorfolio, bool hasISINClaim, bool hasCUSIPClaim)
        {
            FilterDefinition<ScreeningData> query = FilterDefinition<ScreeningData>.Empty;
            var criterias = filterCriteria
                .Select(x => x.CriteriaEntityField)
                .Concat(new[]
                {
                    "_id",
                    "CompanyName",
                    "CompanyNameLowerCase",
                    "CompanyNameUrlFriendly",
                    "CapitalIqId",
                    "Company type",
                    "Template Type",

                    "UniverseIds",
                    "MainExchange",
                    "MainTicker",

                    nameof(ScreeningData.ResearchStates),
                })
                .Distinct()
                .ToArray();

            if (filter != null)
            {
                var selectedUniverse = new List<int>();
                if (filter.UniverseId != null)// preprocess universes
                {
                    accountUniverses.Where(p => p.Name == filter.UniverseName)
                                    .ToList()
                                    .ForEach(x => selectedUniverse.Add(x.Id));
                }

                ApplyFilterSettings(accountUniverseIds, filter, selectedUniverse, companiesInPorfolio, ref query);
            }
            var cron = new Utils.Cronometer();
            var companyData = _screeningDataCollection.Find(query).Project(GetProjectionDefinitionListOfStrings(criterias)).ToList();
            cron.Mark("loading");

            foreach (BsonDocument doc in companyData)
            {
                if (filterCriteria.Exists(x => x.Name == ISIN_NAME) && !hasISINClaim && doc.Contains(ISIN_NAME) &&
                    (doc[ISIN_NAME].AsString.StartsWith("US") || doc[ISIN_NAME].AsString.StartsWith("CA")))
                {
                    doc[ISIN_NAME] = "No Access";
                }
                if (filterCriteria.Exists(x => x.Name == CUSIP_NAME) && !hasCUSIPClaim)
                {
                    doc[CUSIP_NAME] = "No Access";
                }
            }

            return MergeBsonDocumentWithPortfolioData(companyData, portfolioDetails);
        }

        private static void ApplyFilterSettings(
            List<int> accountUniverseIds, 
            Filter filter,
            List<int> selectedUniverse, 
            List<Guid> companiesInPorfolio,
            ref FilterDefinition<ScreeningData> query)
        {
            if (filter == null)
                return;

            var queryBuilder = new List<FilterDefinition<ScreeningData>>();
            
            if (filter.UniverseId != null)
            {
                queryBuilder.Add(Builders<ScreeningData>.Filter.AnyIn(x => x.UniverseIds, selectedUniverse));
            }
            else if (filter.PortfolioId == null)
            {
                queryBuilder.Add(Builders<ScreeningData>.Filter.AnyIn(x => x.UniverseIds, accountUniverseIds));
            }

            if (!string.IsNullOrWhiteSpace(filter.PeerGroupName))
            {
                queryBuilder.Add(Builders<ScreeningData>.Filter.Eq("Peer Group", filter.PeerGroupName));
            }

            if (!string.IsNullOrWhiteSpace(filter.RegionName))
            {
                queryBuilder.Add(Builders<ScreeningData>.Filter.Eq("Region", filter.RegionName));
            }

            if (!string.IsNullOrWhiteSpace(filter.CountryName))
            {
                queryBuilder.Add(Builders<ScreeningData>.Filter.Eq("Country", filter.CountryName));
            }
            
            if (filter.CompanyType != null)
            {
                var selCompanyType = (CompanyTypeEnum)filter.CompanyType;
                var esgResearchTypes = new BsonArray(new string[] { "Core", "Comprehensive" });
                                
                if (selCompanyType.HasFlag(CompanyTypeEnum.Public) && !selCompanyType.HasFlag(CompanyTypeEnum.Private))
                    queryBuilder.Add(Builders<ScreeningData>.Filter.Eq("Company type", "Public"));
                
                if (!selCompanyType.HasFlag(CompanyTypeEnum.Public) && selCompanyType.HasFlag(CompanyTypeEnum.Private))
                    queryBuilder.Add(Builders<ScreeningData>.Filter.Eq("Company type", "Private"));
                                
                if (selCompanyType.HasFlag(CompanyTypeEnum.Comprehensive) && !selCompanyType.HasFlag(CompanyTypeEnum.Core))
                    queryBuilder.Add(Builders<ScreeningData>.Filter.Eq("Research Type", "Comprehensive"));

                if (!selCompanyType.HasFlag(CompanyTypeEnum.Comprehensive) && selCompanyType.HasFlag(CompanyTypeEnum.Core))
                    queryBuilder.Add(Builders<ScreeningData>.Filter.Eq("Research Type", "Core"));

                if (selCompanyType.HasFlag(CompanyTypeEnum.Comprehensive) && selCompanyType.HasFlag(CompanyTypeEnum.Core))
                    queryBuilder.Add(Builders<ScreeningData>.Filter.In("Research Type", esgResearchTypes));
            }

            if (filter.PortfolioId != null)
            {
                // get companies in that portfolio
                queryBuilder.Add(Builders<ScreeningData>.Filter.In(x => x.Id, companiesInPorfolio));
            }

            query = Builders<ScreeningData>.Filter.And(queryBuilder);
        }

        private static List<BsonDocument> MergeBsonDocumentWithPortfolioData(List<BsonDocument> datas, List<PortfolioDetails> details)
        {
            var returnList = new List<BsonDocument>();

            if (details == null)
            {
                return datas;
            }

            foreach (var security in details)
            {//todo   Andrei Pantazi  this could be optimized
                var bsonDocuments = datas.Where(a => security.CompanyId == a["_id"].AsGuid);

                if (bsonDocuments != null)
                {
                    foreach (var document in bsonDocuments)
                    {
                        var newDocument = new BsonDocument { document };
                        newDocument.Add(new BsonElement("SecurityID", security.DisplayInfo.SecurityIdentifier));
                        newDocument.Add(new BsonElement("SecurityName", security.DisplayInfo.CustomSecurityIdentifierName ?? string.Empty));
                        returnList.Add(newDocument);
                    }
                }
            }
            return returnList;
        }
    }
}
